package com.coderworm.tcp2;

import java.io.*;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.zip.CRC32;

/**
 * 带用户名密码验证的RTCM TCP客户端
 * 安全特性：密码SHA-256加盐传输、身份验证请求、RTCM文件合法性校验
 * 完整流程：生成测试RTCM文件 → 密码密文计算 → 身份验证 → 文件发送
 */
public class RtcmTcpClient2 {
    // 1. 服务端配置（必须与RtcmTcpServer完全一致）
    private static final String SERVER_IP = "localhost"; // 服务端IP（本地测试用localhost，远程需填实际IP）
    private static final int SERVER_PORT = 8887; // 服务端端口，与服务端监听端口一致
    private static final String SALT = "RtcmSecure2025_#"; // 密码加盐值，需与服务端SALT完全相同

    // 2. 客户端身份信息（授权账号，需在服务端AUTH_USERS中存在）
    private static final String USER_NAME = "rtcm_device_001"; // 授权用户名
    private static final String PASSWORD = "SecurePwd@2025"; // 授权密码（仅本地存储明文，传输时用SHA-256密文）

    // 3. 测试RTCM文件配置
    private static final String TEST_RTCM_FILE = "test_rtcm_data2.bin"; // 本地测试RTCM文件名
    private static final int TEST_FILE_SIZE = 1024 * 40; // 测试文件大小：30KB（可调整，建议不超过1GB）

    public static void main(String[] args) {
        try {
            // 步骤1：生成符合RTCM格式的测试文件（含0xD3帧头，模拟真实设备文件）
            boolean isFileCreated = createRtcmTestFile(TEST_RTCM_FILE, TEST_FILE_SIZE);
            if (!isFileCreated) {
                System.err.println("测试RTCM文件生成失败，无法继续发送");
                return;
            }

            // 步骤2：计算密码的SHA-256密文（加盐，避免明文传输）
            String pwdHash = calculateSha256(PASSWORD, SALT);
            System.out.println("密码SHA-256密文（加盐）：" + pwdHash);

            // 步骤3：连接服务端，完成身份验证并发送文件
            connectAndSendFile(TEST_RTCM_FILE, USER_NAME, pwdHash);

        } catch (Exception e) {
            System.err.println("客户端运行异常：" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 生成符合RTCM格式的测试文件
     * 关键特征：首字节为0xD3（RTCM标准帧头），其余为随机二进制数据（模拟观测值/导航电文）
     *
     * @param filePath 生成文件的保存路径
     * @param fileSize 生成文件的大小（单位：字节）
     * @return true：生成成功；false：生成失败
     */
    private static boolean createRtcmTestFile(String filePath, int fileSize) {
        // 校验参数：文件大小至少1字节（需包含0xD3帧头）
        if (fileSize < 1) {
            System.err.println("测试文件大小不能小于1字节（需包含RTCM帧头）");
            return false;
        }

        File testFile = new File(filePath);
        // 若文件已存在，先删除（避免使用旧文件导致校验失败）
        if (testFile.exists()) {
            boolean isDeleted = testFile.delete();
            if (!isDeleted) {
                System.err.println("旧测试文件删除失败：" + filePath);
                return false;
            }
        }

        // 写入RTCM格式数据
        try (OutputStream fos = new FileOutputStream(testFile);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {

            // 1. 写入RTCM标准帧头：0xD3（必须包含，否则服务端会判定为非法文件）
            bos.write(0xD3);
            // 2. 生成剩余字节的随机二进制数据（模拟RTCM文件中的卫星观测值、导航电文等）
            byte[] randomData = new byte[fileSize - 1]; // 减1是因为已写入1字节帧头
            Random random = new Random();
            random.nextBytes(randomData); // 填充随机字节，模拟真实数据
            bos.write(randomData);

            // 强制刷新缓冲区，确保数据写入磁盘
            bos.flush();
            System.out.println("\n测试RTCM文件生成成功：");
            System.out.println("文件路径：" + testFile.getAbsolutePath());
            System.out.println("文件大小：" + testFile.length() + " 字节");
            System.out.println("文件首字节（RTCM帧头）：0x" + Integer.toHexString(testFile.length() > 0 ? new FileInputStream(testFile).read() : -1).toUpperCase());
            return true;

        } catch (IOException e) {
            System.err.println("生成测试文件异常：" + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 计算字符串的SHA-256哈希值（加盐）
     * 与服务端calculateSha256方法完全一致，确保密文校验通过
     *
     * @param content 原始内容（如密码）
     * @param salt    加盐值（需与服务端相同）
     * @return SHA-256哈希值（小写16进制字符串）
     */
    private static String calculateSha256(String content, String salt) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            // 加盐：原始内容 + 盐值（防止彩虹表破解）
            byte[] inputBytes = (content + salt).getBytes(StandardCharsets.UTF_8);
            byte[] hashBytes = digest.digest(inputBytes);

            // 转为小写16进制字符串（服务端存储的是小写，需保持一致）
            StringBuilder sb = new StringBuilder();
            for (byte b : hashBytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256算法不存在（JDK环境异常）", e);
        }
    }

    /**
     * 连接服务端，完成身份验证并发送RTCM文件
     * 交互流程：建立TCP连接 → 发送用户名密文 → 发送密码密文 → 验证通过 → 发送文件元数据 → 发送文件内容
     *
     * @param filePath 要发送的RTCM文件路径
     * @param userName 授权用户名
     * @param pwdHash  密码SHA-256密文（加盐）
     */
    private static void connectAndSendFile(String filePath, String userName, String pwdHash) throws IOException {
        File rtcmFile = new File(filePath);
        // 校验文件合法性（存在且为正常文件）
        if (!rtcmFile.exists() || !rtcmFile.isFile()) {
            throw new IOException("RTCM文件不存在或非法：" + filePath);
        }

        // 建立TCP连接（try-with-resources自动关闭流和连接）
        try (Socket clientSocket = new Socket(SERVER_IP, SERVER_PORT);
             // 服务端输出流（发送数据：身份信息、文件元数据、文件内容）
             BufferedOutputStream serverOut = new BufferedOutputStream(clientSocket.getOutputStream());
             // 服务端输入流（接收响应：验证结果、文件接收结果）
             BufferedInputStream serverIn = new BufferedInputStream(clientSocket.getInputStream());
             BufferedReader serverReader = new BufferedReader(new InputStreamReader(serverIn, StandardCharsets.UTF_8));
             // 本地RTCM文件输入流（读取文件内容）
             BufferedInputStream fileIn = new BufferedInputStream(new FileInputStream(rtcmFile))) {

            System.out.println("\n已成功连接到服务端：" + SERVER_IP + ":" + SERVER_PORT);

            // -------------------------- 阶段1：发送身份验证信息（用户名+密码密文） --------------------------
            System.out.println("开始身份验证...");
            // 1.1 发送用户名（格式：2字节长度 + UTF-8内容）
            sendStringToServer(serverOut, userName, "用户名");
            // 1.2 发送密码密文（格式：2字节长度 + UTF-8内容，密文为SHA-256加盐值）
            sendStringToServer(serverOut, pwdHash, "密码密文");

            // 1.3 接收服务端身份验证响应（响应末尾含\n，用readLine读取）
            String authResponse = serverReader.readLine();
            if (authResponse == null) {
                throw new IOException("身份验证失败：服务端无响应");
            }
            // 验证响应是否成功（服务端成功响应前缀为"AUTH_SUCCESS"）
            if (!authResponse.startsWith("AUTH_SUCCESS")) {
                throw new IOException("身份验证失败：" + authResponse);
            }
            System.out.println("身份验证通过：" + authResponse);

            // -------------------------- 阶段2：发送文件元数据（文件名、文件大小、CRC32） --------------------------
            System.out.println("\n开始发送文件元数据...");
            String fileName = rtcmFile.getName();
            long fileTotalSize = rtcmFile.length();
            long fileCrc32 = calculateFileCrc32(rtcmFile); // 计算文件CRC32，用于服务端校验完整性

            // 2.1 发送文件名（格式：2字节长度 + UTF-8内容）
            sendStringToServer(serverOut, fileName, "文件名");
            // 2.2 发送文件总大小（格式：8字节，无符号长整型，大端序）
            sendLongToServer(serverOut, fileTotalSize, "文件大小");
            // 2.3 发送文件CRC32校验码（格式：4字节，无符号整型，大端序）
            sendIntToServer(serverOut, (int) fileCrc32, "CRC32校验码");

            System.out.println("文件元数据发送完成：");
            System.out.println("文件名：" + fileName);
            System.out.println("文件大小：" + fileTotalSize + " 字节");
            System.out.println("文件CRC32：" + fileCrc32);

            // -------------------------- 阶段3：发送文件内容 --------------------------
            System.out.println("\n开始发送文件内容...");
            byte[] buffer = new byte[4096]; // 4KB缓冲区（平衡内存占用和IO效率）
            int bytesRead; // 每次从文件读取的字节数
            long totalSent = 0; // 已发送的总字节数（用于进度显示）

            // 循环读取文件并发送，直到文件读取完毕
            while ((bytesRead = fileIn.read(buffer)) != -1) {
                serverOut.write(buffer, 0, bytesRead); // 发送本次读取的字节
                totalSent += bytesRead;

                // 显示发送进度（每发送10%更新一次，避免频繁打印）
                int progress = (int) ((totalSent * 100.0) / fileTotalSize);
                if (progress % 10 == 0 && (totalSent == 0 || totalSent == fileTotalSize || progress != (int) (((totalSent - bytesRead) * 100.0) / fileTotalSize))) {
                    System.out.println("文件发送进度：" + progress + "%（已发送：" + totalSent + "/" + fileTotalSize + " 字节）");
                }
            }

            // 强制刷新缓冲区，确保所有文件内容发送到服务端（避免缓冲导致数据残留）
            serverOut.flush();
            System.out.println("文件内容发送完成，等待服务端校验结果...");

            // -------------------------- 阶段4：接收服务端文件处理结果 --------------------------
            String fileResponse = serverReader.readLine();
            if (fileResponse == null) {
                throw new IOException("文件处理失败：服务端无响应");
            }
            // 判断文件接收是否成功（服务端成功响应前缀为"SUCCESS"）
            if (fileResponse.startsWith("SUCCESS")) {
                System.out.println("\n文件发送成功！服务端反馈：" + fileResponse);
            } else {
                throw new IOException("文件发送失败：" + fileResponse);
            }

        } catch (IOException e) {
            System.err.println("\n文件发送异常：" + e.getMessage());
            throw e; // 抛出异常，让上层感知失败
        }
    }

    /**
     * 发送字符串到服务端（格式：2字节长度 + UTF-8内容）
     *
     * @param out      服务端输出流
     * @param content  要发送的字符串
     * @param dataDesc 数据描述（如"用户名"，用于异常提示）
     */
    private static void sendStringToServer(BufferedOutputStream out, String content, String dataDesc) throws IOException {
        byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
        int contentLen = contentBytes.length;

        // 1. 发送字符串长度（2字节，无符号短整型，大端序）
        byte[] lenBytes = ByteBuffer.allocate(2)
                .order(ByteOrder.BIG_ENDIAN) // 大端序，与服务端解析逻辑一致
                .putShort((short) contentLen)
                .array();
        out.write(lenBytes);

        // 2. 发送字符串内容（UTF-8编码）
        out.write(contentBytes);
        out.flush(); // 关键：强制刷新缓冲区，避免数据滞留（否则服务端收不到）
        System.out.println("已发送" + dataDesc + "：长度=" + contentLen + "字节，内容=" + content);
    }

    /**
     * 发送长整型数值到服务端（格式：8字节，无符号长整型，大端序）
     *
     * @param out      服务端输出流
     * @param value    要发送的长整型值
     * @param dataDesc 数据描述（如"文件大小"，用于异常提示）
     */
    private static void sendLongToServer(BufferedOutputStream out, long value, String dataDesc) throws IOException {
        byte[] valueBytes = ByteBuffer.allocate(8)
                .order(ByteOrder.BIG_ENDIAN)
                .putLong(value)
                .array();
        out.write(valueBytes);
        System.out.println("已发送" + dataDesc + "：" + value + "（二进制长度：8字节）");
    }

    /**
     * 发送整型数值到服务端（格式：4字节，无符号整型，大端序）
     *
     * @param out      服务端输出流
     * @param value    要发送的整型值
     * @param dataDesc 数据描述（如"CRC32校验码"，用于异常提示）
     */
    private static void sendIntToServer(BufferedOutputStream out, int value, String dataDesc) throws IOException {
        byte[] valueBytes = ByteBuffer.allocate(4)
                .order(ByteOrder.BIG_ENDIAN)
                .putInt(value)
                .array();
        out.write(valueBytes);
        System.out.println("已发送" + dataDesc + "：" + value + "（二进制长度：4字节）");
    }

    /**
     * 计算文件的CRC32校验码（与服务端校验逻辑一致）
     *
     * @param file 要计算的文件
     * @return CRC32校验码（无符号长整型）
     */
    private static long calculateFileCrc32(File file) throws IOException {
        CRC32 crc32 = new CRC32();
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                crc32.update(buffer, 0, bytesRead); // 逐段更新CRC值
            }
        }
        return crc32.getValue();
    }
}